/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.geode.security.generator;

import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.logging.log4j.Logger;

import org.apache.geode.internal.logging.LogService;
import org.apache.geode.security.AuthInitialize;
import org.apache.geode.security.Authenticator;
import org.apache.geode.security.templates.DummyAuthenticator;
import org.apache.geode.security.templates.LdapUserAuthenticator;
import org.apache.geode.security.templates.PKCSAuthenticator;

/**
 * Encapsulates obtaining valid and invalid credentials. Implementations will be
 * for different kinds of authentication schemes.
 * 
 * @since GemFire 5.5
 */
public abstract class CredentialGenerator {

  private static final Logger logger = LogService.getLogger();

  /**
   * A set of properties that should be added to the Gemfire system properties
   * before using the authentication module.
   */
  private Properties systemProperties = null;

  /**
   * A set of properties that should be added to the java system properties
   * before using the authentication module.
   */
  protected Properties javaProperties = null;

  /**
   * A factory method to create a new instance of an {@link CredentialGenerator}
   * for the given {@link ClassCode}. Caller is supposed to invoke
   * {@link CredentialGenerator#init} immediately after obtaining the instance.
   * 
   * @param  classCode
   *         the {@code ClassCode} of the {@code CredentialGenerator}
   *         implementation
   * 
   * @return an instance of {@code CredentialGenerator} for the given class
   *         code
   */
  public static CredentialGenerator create(final ClassCode classCode) {
    switch (classCode.classType) {
      // Removing dummy one to reduce test run times
      // case ClassCode.ID_DUMMY:
      // return new DummyCredentialGenerator();
      case ClassCode.ID_LDAP:
        return new LdapUserCredentialGenerator();
        // case ClassCode.ID_SSL:ø
        // return new SSLCredentialGenerator();
      case ClassCode.ID_PKCS:
        return new PKCSCredentialGenerator();
      default:
        return null;
    }
  }

  /**
   * Initialize the credential generator.
   *
   * @throws IllegalArgumentException when there is a problem during
   *         initialization
   */
  public void init() throws IllegalArgumentException {
    this.systemProperties = initialize();
    logger.info("Generating CredentialGenerator with {}", this.systemProperties);
  }

  /**
   * @return A set of extra properties that should be added to Gemfire system
   *         properties when not null.
   */
  public Properties getSystemProperties() {
    return this.systemProperties;
  }

  /**
   * @return A set of extra properties that should be added to Gemfire system
   *         properties when not null.
   */
  public Properties getJavaProperties() {
    return this.javaProperties;
  }

  /**
   * The {@link ClassCode} of this particular implementation.
   * 
   * @return the {@code ClassCode}
   */
  public abstract ClassCode classCode();

  /**
   * The name of the {@link AuthInitialize} factory function that should be used
   * in conjunction with the credentials generated by this generator.
   * 
   * @return name of the {@code AuthInitialize} factory function
   */
  public abstract String getAuthInit();

  /**
   * The name of the {@link Authenticator} factory function that should be used
   * in conjunction with the credentials generated by this generator.
   * 
   * @return name of the {@code Authenticator} factory function
   */
  public abstract String getAuthenticator();

  /**
   * Get a set of valid credentials generated using the given index.
   */
  public abstract Properties getValidCredentials(final int index);

  /**
   * Get a set of valid credentials for the given {@link Principal}.
   * 
   * @return credentials for the given {@code Principal} or null if none
   *         possible.
   */
  public abstract Properties getValidCredentials(final Principal principal);

  /**
   * Get a set of invalid credentials generated using the given index.
   */
  public abstract Properties getInvalidCredentials(final int index);

  /**
   * Initialize the credential generator. This is provided separately from the
   * {@link #init()} method for convenience of implementations so that they do not
   * need to store in {@link #systemProperties}. The latter is convenient for the users
   * who do not need to store these properties rather can obtain it later by
   * invoking {@link #getSystemProperties()}
   *
   * <p>Required to be implemented by concrete classes that implement this abstract
   * class.
   *
   * @return A set of extra properties that should be added to Gemfire system
   *         properties when not null.
   *
   * @throws IllegalArgumentException when there is a problem during
   *         initialization
   */
  protected abstract Properties initialize() throws IllegalArgumentException;

  /**
   * Enumeration for various {@link CredentialGenerator} implementations.
   *
   * <p>The following schemes are supported as of now:
   * {@code DummyAuthenticator}, {@code LdapUserAuthenticator},
   * {@code PKCSAuthenticator}. In addition SSL socket mode with mutual
   * authentication is also supported.
   *
   * <p>To add a new authentication scheme the following needs to be done:
   * <ul>
   * <li>Add implementations for {@link AuthInitialize} and
   * {@link Authenticator} classes for clients/peers.</li>
   * <li>Add a new enumeration value for the scheme in this class. Notice the
   * size of {@code VALUES} array and increase that if it is getting
   * overflowed. Note the methods and fields for existing schemes and add for
   * the new one in a similar manner.</li>
   * <li>Add an implementation for {@link CredentialGenerator}.</li>
   * <li>Modify the CredentialGenerator.Factory#create [no such Factory exists] method to add
   * creation of an instance of the new implementation for the
   * {@code ClassCode} enumeration value.</li>
   * </ul>
   *
   * <p>All security dunit tests will automagically start testing the new
   * implementation after this.
   *
   * @since GemFire 5.5
   */
  public static final class ClassCode {

    private static byte nextOrdinal = 0;

    private static final byte ID_DUMMY = 1;
    private static final byte ID_LDAP = 2;
    private static final byte ID_PKCS = 3;
    private static final byte ID_SSL = 4;

    private static final ClassCode[] VALUES = new ClassCode[10];
    private static final Map CODE_NAME_MAP = new HashMap();

    public static final ClassCode DUMMY = new ClassCode(DummyAuthenticator.class.getName() + ".create", ID_DUMMY);
    public static final ClassCode LDAP = new ClassCode(LdapUserAuthenticator.class.getName() + ".create", ID_LDAP);
    public static final ClassCode PKCS = new ClassCode(PKCSAuthenticator.class.getName() + ".create", ID_PKCS);
    public static final ClassCode SSL = new ClassCode("SSL", ID_SSL);

    /** The name of this class. */
    private final String name;

    /** byte used as ordinal to represent this class */
    private final byte ordinal;

    /**
     * One of the following: ID_DUMMY, ID_LDAP, ID_PKCS
     */
    private final byte classType;

    /** Creates a new instance of class code. */
    private ClassCode(final String name, final byte classType) {
      this.name = name;
      this.classType = classType;
      this.ordinal = nextOrdinal++;
      VALUES[this.ordinal] = this;
      CODE_NAME_MAP.put(name, this);
    }

    public boolean isDummy() {
      return this.classType == ID_DUMMY;
    }

    public boolean isLDAP() {
      return this.classType == ID_LDAP;
    }

    public boolean isPKCS() {
      return this.classType == ID_PKCS;
    }

    public boolean isSSL() {
      return this.classType == ID_SSL;
    }

    /**
     * Returns the {@code ClassCode} represented by specified ordinal.
     */
    public static ClassCode fromOrdinal(final byte ordinal) {
      return VALUES[ordinal];
    }

    /**
     * Returns the {@code ClassCode} represented by specified string.
     */
    public static ClassCode parse(final String operationName) {
      return (ClassCode) CODE_NAME_MAP.get(operationName);
    }

    /**
     * Returns all the possible values.
     */
    public static List getAll() {
      final List codes = new ArrayList();
      for (Iterator iter = CODE_NAME_MAP.values().iterator(); iter.hasNext();) {
        codes.add(iter.next());
      }
      return codes;
    }

    /**
     * Returns the ordinal for this operation code.
     *
     * @return the ordinal of this operation.
     */
    public byte toOrdinal() {
      return this.ordinal;
    }

    /**
     * Returns a string representation for this operation.
     *
     * @return the name of this operation.
     */
    @Override
    public final String toString() {
      return this.name;
    }

    /**
     * Indicates whether other object is same as this one.
     *
     * @return true if other object is same as this one.
     */
    @Override
    public final boolean equals(final Object obj) {
      if (obj == this) {
        return true;
      }
      if (!(obj instanceof ClassCode)) {
        return false;
      }
      final ClassCode other = (ClassCode)obj;
      return other.ordinal == this.ordinal;
    }

    /**
     * Indicates whether other {@code ClassCode} is same as this one.
     *
     * @return true if other {@code ClassCode} is same as this one.
     */
    public final boolean equals(final ClassCode opCode) {
      return opCode != null && opCode.ordinal == this.ordinal;
    }

    /**
     * Returns a hash code value for this {@code ClassCode} which is the
     * same as its ordinal.
     *
     * @return the ordinal of this operation.
     */
    @Override
    public final int hashCode() {
      return this.ordinal;
    }
  }
}
